<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>自适应高度虚拟列表 - Vue2实现</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
    }
    
    body {
      background: linear-gradient(135deg, #1d2b64, #2c3e50);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20px;
      color: #f0f0f0;
    }
    
    .container {
      width: 100%;
      max-width: 1200px;
    }
    
    header {
      text-align: center;
      margin-bottom: 30px;
      padding: 20px;
      background: rgba(30, 40, 70, 0.7);
      border-radius: 15px;
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
    }
    
    h1 {
      font-size: 2.6rem;
      margin-bottom: 15px;
      color: #64b5f6;
      text-shadow: 0 0 10px rgba(100, 181, 246, 0.4);
    }
    
    .subtitle {
      font-size: 1.2rem;
      color: #90caf9;
      max-width: 800px;
      margin: 0 auto;
      line-height: 1.6;
    }
    
    .main-content {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 25px;
    }
    
    .card {
      background: rgba(25, 35, 60, 0.8);
      border-radius: 15px;
      overflow: hidden;
      box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
      border: 1px solid rgba(100, 150, 200, 0.2);
    }
    
    .card-header {
      background: rgba(40, 55, 90, 0.9);
      padding: 20px;
      border-bottom: 1px solid rgba(100, 150, 200, 0.3);
    }
    
    .card-title {
      font-size: 1.4rem;
      color: #bb86fc;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    
    .controls {
      display: flex;
      gap: 15px;
      margin-top: 15px;
      flex-wrap: wrap;
    }
    
    .control-group {
      display: flex;
      gap: 10px;
      align-items: center;
    }
    
    label {
      font-size: 0.9rem;
      color: #90a4ae;
    }
    
    input, select, button {
      background: rgba(20, 30, 50, 0.7);
      border: 1px solid rgba(100, 150, 200, 0.3);
      border-radius: 6px;
      padding: 8px 12px;
      color: #e0e0e0;
      outline: none;
    }
    
    button {
      background: linear-gradient(to right, #2196f3, #21cbf3);
      border: none;
      color: white;
      cursor: pointer;
      transition: all 0.3s;
      font-weight: bold;
    }
    
    button:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
    }
    
    /* 虚拟列表容器 */
    .virtual-container {
      height: 600px;
      position: relative;
      overflow-y: auto;
      background: rgba(20, 30, 50, 0.6);
    }
    
    /* 滚动内容占位 */
    .scroll-content {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: var(--total-height);
    }
    
    /* 可视区域 */
    .viewport {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      transform: translateY(var(--offset));
    }
    
    /* 消息项样式 - 高度不固定 */
    .message-item {
      padding: 20px;
      border-bottom: 1px solid rgba(100, 150, 200, 0.15);
      transition: all 0.3s;
      background: rgba(30, 45, 75, 0.7);
      margin: 10px;
      border-radius: 10px;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
    }
    
    .message-item:hover {
      background: rgba(40, 65, 100, 0.8);
      transform: translateY(-3px);
      box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
    }
    
    .message-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 15px;
    }
    
    .user-info {
      display: flex;
      align-items: center;
    }
    
    .avatar {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      background: linear-gradient(135deg, #64b5f6, #bb86fc);
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 15px;
      font-weight: bold;
      font-size: 1.2rem;
      color: white;
    }
    
    .username {
      font-weight: bold;
      color: #64b5f6;
      font-size: 1.1rem;
    }
    
    .timestamp {
      color: #81c784;
      font-size: 0.85rem;
    }
    
    .message-content {
      color: #e0e0e0;
      line-height: 1.6;
      margin-bottom: 15px;
    }
    
    .message-image {
      width: 100%;
      border-radius: 8px;
      margin: 10px 0;
      background: linear-gradient(135deg, #3a7bd5, #00d2ff);
      height: 150px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: rgba(255, 255, 255, 0.8);
      font-weight: bold;
    }
    
    .message-actions {
      display: flex;
      gap: 15px;
      padding-top: 10px;
      border-top: 1px solid rgba(100, 150, 200, 0.2);
    }
    
    .action-btn {
      display: flex;
      align-items: center;
      gap: 5px;
      color: #90a4ae;
      font-size: 0.9rem;
      cursor: pointer;
      transition: color 0.3s;
    }
    
    .action-btn:hover {
      color: #64b5f6;
    }
    
    /* 信息面板 */
    .info-panel {
      padding: 25px;
    }
    
    .info-title {
      color: #bb86fc;
      margin-bottom: 20px;
      padding-bottom: 10px;
      border-bottom: 1px solid rgba(100, 150, 200, 0.3);
      font-size: 1.3rem;
    }
    
    .info-content {
      line-height: 1.8;
      color: #c5cae9;
    }
    
    .algorithm-steps {
      list-style: none;
      margin: 20px 0;
      counter-reset: step-counter;
    }
    
    .algorithm-steps li {
      margin-bottom: 25px;
      padding-left: 40px;
      position: relative;
    }
    
    .algorithm-steps li:before {
      counter-increment: step-counter;
      content: counter(step-counter);
      position: absolute;
      left: 0;
      top: 0;
      width: 30px;
      height: 30px;
      background: linear-gradient(135deg, #2196f3, #21cbf3);
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: bold;
      color: white;
    }
    
    .complexity-table {
      width: 100%;
      border-collapse: collapse;
      margin: 20px 0;
      background: rgba(30, 45, 75, 0.5);
      border-radius: 8px;
      overflow: hidden;
    }
    
    .complexity-table th {
      background: rgba(40, 55, 90, 0.9);
      padding: 12px 15px;
      text-align: left;
      color: #bb86fc;
    }
    
    .complexity-table td {
      padding: 12px 15px;
      border-bottom: 1px solid rgba(100, 150, 200, 0.2);
    }
    
    .complexity-table tr:last-child td {
      border-bottom: none;
    }
    
    .performance-metric {
      color: #4db6ac;
      font-weight: bold;
    }
    
    footer {
      text-align: center;
      margin-top: 30px;
      padding: 20px;
      color: rgba(200, 220, 255, 0.7);
      font-size: 0.95rem;
    }
    
    /* 滚动条美化 */
    .virtual-container::-webkit-scrollbar {
      width: 10px;
    }
    
    .virtual-container::-webkit-scrollbar-track {
      background: rgba(20, 30, 50, 0.5);
      border-radius: 4px;
    }
    
    .virtual-container::-webkit-scrollbar-thumb {
      background: linear-gradient(to bottom, #64b5f6, #bb86fc);
      border-radius: 4px;
    }
    
    .virtual-container::-webkit-scrollbar-thumb:hover {
      background: linear-gradient(to bottom, #42a5f5, #9d65ff);
    }
  </style>
</head>
<body>
  <div id="app" class="container">
    <header>
      <h1>自适应高度虚拟列表</h1>
      <p class="subtitle">Vue2实现动态高度项目渲染，解决内容高度不固定问题，实现高性能无闪动加载</p>
    </header>
    
    <div class="main-content">
      <div class="card">
        <div class="card-header">
          <div class="card-title">
            自适应高度虚拟列表演示
            <span>总项目: {{ totalItems.toLocaleString() }}</span>
          </div>
          
          <div class="controls">
            <div class="control-group">
              <label>数据类型:</label>
              <select v-model="contentType">
                <option value="mixed">混合内容</option>
                <option value="short">短文本</option>
                <option value="long">长文本</option>
                <option value="images">带图片</option>
              </select>
            </div>
            
            <div class="control-group">
              <label>缓冲区:</label>
              <input type="range" min="0" max="20" v-model="bufferSize">
              <span>{{ bufferSize }} 项</span>
            </div>
            
            <button @click="resetList">重置列表</button>
            <button @click="prependItems">添加历史数据</button>
          </div>
        </div>
        
        <div 
          class="virtual-container" 
          ref="scrollContainer"
          @scroll="handleScroll"
        >
          <!-- 滚动内容占位 -->
          <div 
            class="scroll-content" 
            :style="{ '--total-height': totalHeight + 'px' }"
          ></div>
          
          <!-- 可视区域 -->
          <div 
            class="viewport" 
            :style="{ '--offset': offset + 'px' }"
          >
            <!-- 渲染可视区域内的项目 -->
            <div 
              v-for="item in visibleItems" 
              :key="item.id" 
              class="message-item"
              :ref="'item_' + item.id"
              :data-id="item.id"
            >
              <div class="message-header">
                <div class="user-info">
                  <div class="avatar">{{ item.user.charAt(0) }}</div>
                  <div>
                    <div class="username">{{ item.user }}</div>
                    <div class="timestamp">{{ formatTime(item.timestamp) }}</div>
                  </div>
                </div>
                <div class="timestamp">ID: {{ item.id }}</div>
              </div>
              
              <div class="message-content">
                <p>{{ item.content }}</p>
                
                <div v-if="item.hasImage" class="message-image">
                  📷 图片内容 ({{ item.imageSize }})
                </div>
              </div>
              
              <div class="message-actions">
                <div class="action-btn">👍 {{ item.likes }}</div>
                <div class="action-btn">💬 {{ item.comments }}</div>
                <div class="action-btn">↗️ 分享</div>
              </div>
            </div>
          </div>
        </div>
      </div>
      
      <div class="card">
        <div class="card-header">
          <div class="card-title">实现原理与技术细节</div>
        </div>
        
        <div class="info-panel">
          <h2 class="info-title">自适应高度虚拟列表算法</h2>
          
          <ol class="algorithm-steps">
            <li><strong>位置预估</strong> - 初始使用预估高度计算位置</li>
            <li><strong>实际测量</strong> - 元素渲染后测量实际高度</li>
            <li><strong>位置校正</strong> - 更新位置缓存并调整后续元素位置</li>
            <li><strong>滚动补偿</strong> - 高度变化时动态调整滚动位置</li>
            <li><strong>智能缓存</strong> - 存储已测量元素的高度避免重复计算</li>
          </ol>
          
          <h2 class="info-title">性能优化策略</h2>
          
          <table class="complexity-table">
            <thead>
              <tr>
                <th>策略</th>
                <th>时间复杂度</th>
                <th>效果</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>二分查找定位</td>
                <td>O(log n)</td>
                <td>快速确定可视区域</td>
              </tr>
              <tr>
                <td>位置缓存</td>
                <td>O(1) 读取</td>
                <td>避免重复计算</td>
              </tr>
              <tr>
                <td>批量更新</td>
                <td>O(k) k=变化项</td>
                <td>最小化布局计算</td>
              </tr>
              <tr>
                <td>请求动画帧</td>
                <td>-</td>
                <td>避免布局抖动</td>
              </tr>
            </tbody>
          </table>
          
          <h2 class="info-title">性能指标</h2>
          
          <div class="info-content">
            <p>当前渲染项目: <span class="performance-metric">{{ visibleItems.length }}</span></p>
            <p>位置缓存数量: <span class="performance-metric">{{ Object.keys(positionCache).length }}</span></p>
            <p>总计算高度: <span class="performance-metric">{{ Math.round(totalHeight/1000) }}k px</span></p>
            <p>平均项目高度: <span class="performance-metric">{{ avgItemHeight.toFixed(1) }} px</span></p>
            <p>高度变化率: <span class="performance-metric">{{ heightChangeRate.toFixed(1) }}%</span></p>
          </div>
          
          <h2 class="info-title">技术挑战与解决方案</h2>
          
          <div class="info-content">
            <p><strong>挑战：</strong> 内容动态加载导致高度变化</p>
            <p><strong>解决方案：</strong> 使用ResizeObserver监测元素变化</p>
            
            <p><strong>挑战：</strong> 快速滚动时性能问题</p>
            <p><strong>解决方案：</strong> 滚动节流 + 动态缓冲区调整</p>
            
            <p><strong>挑战：</strong> 大量位置缓存占用内存</p>
            <p><strong>解决方案：</strong> LRU缓存策略 + 预估算法</p>
          </div>
        </div>
      </div>
    </div>
    
    <footer>
      自适应高度虚拟列表实现 | 解决动态内容高度问题 | 高性能渲染 | 无闪动加载 | Vue2实现
    </footer>
  </div>

  <script>
    new Vue({
      el: '#app',
      data() {
        return {
          totalItems: 5000,
          items: [],
          positionCache: {},
          estimatedHeight: 200,
          scrollTop: 0,
          bufferSize: 5,
          viewportHeight: 600,
          resizeObserver: null,
          contentType: 'mixed',
          lastItemId: 0
        }
      },
      computed: {
        // 计算总高度（基于位置缓存）
        totalHeight() {
          if (this.items.length === 0) return 0;
          
          // 获取最后一个项目的位置信息
          const lastItem = this.items[this.items.length - 1];
          const lastPos = this.positionCache[lastItem.id];
          
          // 如果最后一个项目有缓存，使用它的bottom值
          if (lastPos) {
            return lastPos.bottom;
          }
          
          // 否则使用预估高度计算
          return this.items.length * this.estimatedHeight;
        },
        
        // 计算平均项目高度
        avgItemHeight() {
          const cachedItems = Object.values(this.positionCache);
          if (cachedItems.length === 0) return this.estimatedHeight;
          
          const totalHeight = cachedItems.reduce((sum, pos) => sum + pos.height, 0);
          return totalHeight / cachedItems.length;
        },
        
        // 计算高度变化率
        heightChangeRate() {
          const cachedItems = Object.values(this.positionCache);
          if (cachedItems.length === 0) return 0;
          
          let changes = 0;
          cachedItems.forEach(pos => {
            if (Math.abs(pos.height - this.estimatedHeight) > 10) {
              changes++;
            }
          });
          
          return (changes / cachedItems.length) * 100;
        },
        
        // 获取可见区域的项目
        visibleItems() {
          if (this.items.length === 0) return [];
          
          // 使用位置缓存计算可见区域
          const startIdx = this.findNearestItem(this.scrollTop);
          const endIdx = this.findNearestItem(this.scrollTop + this.viewportHeight);
          
          const start = Math.max(0, startIdx - this.bufferSize);
          const end = Math.min(this.items.length - 1, endIdx + this.bufferSize);
          
          return this.items.slice(start, end + 1);
        },
        
        // 计算可视区域偏移量
        offset() {
          if (this.visibleItems.length === 0) return 0;
          
          const firstVisible = this.visibleItems[0];
          const pos = this.positionCache[firstVisible.id];
          
          // 如果第一个可见项目有位置缓存，使用它的top值
          if (pos) {
            return pos.top;
          }
          
          // 否则使用预估位置
          const idx = this.items.findIndex(item => item.id === firstVisible.id);
          return idx * this.estimatedHeight;
        }
      },
      watch: {
        contentType() {
          this.resetList();
        }
      },
      methods: {
        // 初始化列表
        initList() {
          this.items = this.generateItems(200, 0);
          this.lastItemId = 200;
          
          // 初始化后更新位置缓存
          this.$nextTick(() => {
            this.updateItemPositions();
          });
        },
        
        // 重置列表
        resetList() {
          this.positionCache = {};
          this.initList();
          this.$refs.scrollContainer.scrollTop = 0;
        },
        
        // 添加历史数据
        prependItems() {
          const newItems = this.generateItems(20, -1);
          this.items = [...newItems, ...this.items];
          this.lastItemId -= 20;
          
          // 更新位置缓存
          this.$nextTick(() => {
            this.updateItemPositions();
            
            // 调整滚动位置保持当前视图
            const scrollContainer = this.$refs.scrollContainer;
            const scrollAdjust = newItems.reduce((sum, item) => {
              const pos = this.positionCache[item.id];
              return sum + (pos ? pos.height : this.estimatedHeight);
            }, 0);
            
            scrollContainer.scrollTop += scrollAdjust;
          });
        },
        
        // 生成模拟项目
        generateItems(count, startId) {
          const newItems = [];
          const now = Date.now();
          
          for (let i = 0; i < count; i++) {
            const id = startId >= 0 ? startId + i : this.lastItemId + i;
            
            // 根据内容类型生成不同内容
            let content = '';
            let hasImage = false;
            let imageSize = '';
            
            switch(this.contentType) {
              case 'short':
                content = this.generateShortText();
                break;
              case 'long':
                content = this.generateLongText();
                break;
              case 'images':
                content = this.generateShortText();
                hasImage = true;
                imageSize = `${Math.floor(Math.random() * 3) + 1}MB`;
                break;
              default: // mixed
                if (Math.random() > 0.7) {
                  content = this.generateLongText();
                } else {
                  content = this.generateShortText();
                  if (Math.random() > 0.5) {
                    hasImage = true;
                    imageSize = `${Math.floor(Math.random() * 3) + 1}MB`;
                  }
                }
            }
            
            newItems.push({
              id,
              user: this.generateName(),
              content,
              timestamp: now - (id * 60000),
              likes: Math.floor(Math.random() * 100),
              comments: Math.floor(Math.random() * 20),
              hasImage,
              imageSize
            });
          }
          
          return newItems;
        },
        
        // 生成短文本
        generateShortText() {
          const sentences = [
            '虚拟列表技术解决了大数据量渲染问题',
            '自适应高度处理动态内容',
            '高性能滚动体验',
            '避免页面闪动',
            '优化内存使用',
            '提升用户体验'
          ];
          return sentences[Math.floor(Math.random() * sentences.length)];
        },
        
        // 生成长文本
        generateLongText() {
          const paragraphs = Math.floor(Math.random() * 3) + 1;
          let text = '';
          
          for (let i = 0; i < paragraphs; i++) {
            text += '自适应高度虚拟列表的核心挑战是处理动态内容高度。通过位置预估、实际测量和位置校正三个步骤，我们可以在高度不确定的情况下实现高性能渲染。首先初始化时使用预估高度计算布局，然后当元素渲染后测量实际高度并更新位置缓存，最后调整后续元素的位置。这个过程确保了滚动位置的准确性，同时保持高性能。';
            
            if (i < paragraphs - 1) {
              text += '\n\n';
            }
          }
          
          return text;
        },
        
        // 生成随机名字
        generateName() {
          const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
          return names[Math.floor(Math.random() * names.length)];
        },
        
        // 处理滚动事件
        handleScroll() {
          const container = this.$refs.scrollContainer;
          this.scrollTop = container.scrollTop;
        },
        
        // 查找最近的项目索引
        findNearestItem(scrollTop) {
          // 简单实现 - 实际应使用二分查找
          let low = 0;
          let high = this.items.length - 1;
          let mid;
          
          while (low <= high) {
            mid = Math.floor((low + high) / 2);
            const item = this.items[mid];
            const pos = this.positionCache[item.id];
            
            if (!pos) {
              // 如果没有缓存，使用预估位置
              const estTop = mid * this.estimatedHeight;
              const estBottom = estTop + this.estimatedHeight;
              
              if (scrollTop < estTop) {
                high = mid - 1;
              } else if (scrollTop > estBottom) {
                low = mid + 1;
              } else {
                return mid;
              }
            } else {
              if (scrollTop < pos.top) {
                high = mid - 1;
              } else if (scrollTop > pos.bottom) {
                low = mid + 1;
              } else {
                return mid;
              }
            }
          }
          
          return Math.max(0, Math.min(this.items.length - 1, mid));
        },
        
        // 更新项目位置
        updateItemPositions() {
          requestAnimationFrame(() => {
            this.visibleItems.forEach(item => {
              const el = this.$refs[`item_${item.id}`]?.[0];
              if (el) {
                const rect = el.getBoundingClientRect();
                const containerRect = this.$refs.scrollContainer.getBoundingClientRect();
                const top = rect.top - containerRect.top + this.scrollTop;
                
                // 更新位置缓存
                this.positionCache[item.id] = {
                  top,
                  height: rect.height,
                  bottom: top + rect.height
                };
              }
            });
          });
        },
        
        // 格式化时间
        formatTime(timestamp) {
          const date = new Date(timestamp);
          return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
        }
      },
      mounted() {
        // 初始化视口高度
        this.viewportHeight = this.$refs.scrollContainer.clientHeight;
        
        // 初始化列表
        this.initList();
        
        // 设置ResizeObserver监听元素大小变化
        this.resizeObserver = new ResizeObserver(entries => {
          entries.forEach(entry => {
            const id = entry.target.dataset.id;
            if (id) {
              this.updateItemPosition(id);
            }
          });
        });
        
        // 监听可视区域元素变化
        this.$watch('visibleItems', (newItems) => {
          // 清理之前的监听
          this.resizeObserver.disconnect();
          
          // 监听新元素
          newItems.forEach(item => {
            const el = this.$refs[`item_${item.id}`]?.[0];
            if (el) {
              this.resizeObserver.observe(el);
            }
          });
        }, { immediate: true });
      },
      beforeDestroy() {
        if (this.resizeObserver) {
          this.resizeObserver.disconnect();
        }
      }
    });
  </script>
</body>
</html>